package generator

import (
	"errors"
	"go/build"
	"go/token"
	"reflect"
	"strings"
	"text/template"

	"gitee.com/knowgo/gotool/errs"
	"gitee.com/knowgo/gpa/engine/constants"
	"gitee.com/knowgo/gpa/engine/generator/defines"
	"gitee.com/knowgo/gpa/engine/generator/method"
	"gitee.com/knowgo/gpa/engine/generator/sqlgenerator"
	"gitee.com/knowgo/logo"
	"gitee.com/knowgo/logo/log"
)

type Generator struct {
	Info    *defines.Info
	methods []method.Method
	dialect constants.DIALECT
	logger  logo.Logger
}

func New(dialectName constants.DIALECT, logger logo.Logger) (*Generator, error) {
	if logger == nil {
		logger = log.GetDefaultLogger()
	}
	sqlg, err := sqlgenerator.GetGenerator(dialectName)
	if err != nil {
		return nil, err
	}
	var g = Generator{
		dialect: dialectName,
		logger:  logger,
	}
	g.methods = method.CreateMethods(sqlg, logger)
	return &g, nil
}

func (g *Generator) getMethod(fn *defines.Func) method.Method {
	for _, g := range g.methods {
		if g.Test(fn) {
			return g
		}
	}
	return nil
}

func (g *Generator) loadFuncTemplate() (*template.Template, error) {
	funcs := template.FuncMap{"join": strings.Join}
	tmpl, err := template.ParseFS(templates, "resources/repo.tpl")
	if err != nil {
		return nil, err
	}
	tmpl, err = tmpl.Funcs(funcs).ParseFS(templates,
		"resources/repo-body.tpl",
		"resources/func-truncate.tpl",
		"resources/func-insert.tpl",
		"resources/func-update.tpl",
		"resources/func-delete.tpl",
		"resources/func-find.tpl",
		"resources/func-find-block-read-row.tpl",
		"resources/func-find-block-read-rows.tpl",
		"resources/func-find-block-read-rows-callback.tpl",
		"resources/func-count.tpl")
	if err != nil {
		return nil, err
	}
	return tmpl, nil
}

// Generate 生成数据持久化代码
// @pkgTag 字符串         数据库操作接口定义所在包路径
//		   reflect.Type  任意数据库操作接口实例，例如 repos.Count 的实例，可以使用以下代码
//					     reflect.TypeOf((*repos.Count)(nil)).Elem())
func (g *Generator) Generate(pkgTag interface{}, forceRegenerate bool) error {
	// var fullPackagePath string
	fullPackagePath, ok := pkgTag.(string)
	if !ok {
		typ, ok := pkgTag.(reflect.Type)
		if !ok {
			return errors.New("pkgTag must be repository interface's package pah (string) or repository interface type (e: reflect.TypeOf((*repos.Count)(nil)).Elem())")
		}
		fullPackagePath = typ.PkgPath()
	}
	if err := g.parse(fullPackagePath); err != nil {
		return err
	}
	// 检查源码路径是否存在，不存在则创建
	if err := g.Info.CreateRepositoryDirIfNotExists(); err != nil {
		return err
	}

	// 依据接口定义创建 repository
	tmpl, err := g.loadFuncTemplate()
	if err != nil {
		return err
	}
	totalError := 0
	if err := g.Info.TraverseRepoFiles(func(rf *defines.RepoFile) error {
		errCount, err := g.createRepoFile(rf, tmpl)
		totalError += errCount
		if err != nil {
			panic(err)
		}
		return nil
	}); err != nil {
		return err
	}

	if forceRegenerate || g.Info.IsProvidesChanged() {
		// 重新生成 doc.go
		if err := g.createDocFile(); err != nil {
			return err
		}
		// 重新生成 interface.go
		if err := g.createInterfaceFile(); err != nil {
			return err
		}
		// 生成实现 implement.go
		if err := g.createImplementFile(); err != nil {
			return err
		}
	}
	if totalError > 0 {
		log.Errorf("%d errors when generating method", totalError)
	}
	return nil
}

func (g *Generator) parse(pkg string) error {
	// 解析路径信息
	pkgInfo, err := build.Import(pkg, "", build.FindOnly)
	if err != nil {
		return err
	}
	info := defines.NewInfo(pkg, g.dialect, g.logger)
	info.FSet = token.NewFileSet()
	info.Dir = pkgInfo.Dir
	g.logger.Infof("parsing %s with dialect %s ...", info.Dir, string(g.dialect))

	// 查找全部用户定义的 repostory 接口文件
	if err := info.FindRepoFiles(); err != nil {
		err = errs.NewStackError("search repo define files error: %s", err.Error())
		g.logger.Error(err)
		return err
	}
	g.logger.Infof("found %d repo files", len(info.Files))
	g.logger.Infof("got repos package: %s, named: %s", info.Package, info.PackageName)
	for _, r := range info.Files {
		log.Infof("parsing repo %s", r.Path)
		if err := r.Parse(info.Dialect); err != nil {
			err = errs.NewStackError("parse repo file %s error: %s", r.Path, err.Error())
			g.logger.Error(err)
			return err
		}

		r.AddRuntimePackage()
		r.AddLogoPackage()
	}
	g.Info = info
	return nil
}
